//
//  PLPlatform.m
//  Plankton
//
//  Created by Marc Schlichte on 2008-07-12.
//  Copyright 2008 Framework Labs. All rights reserved.
//

#import "PLPlatform.h"

#import "PLEndpoint.h"


#pragma mark handlers

@implementation PLTargetActionPair

- (id)initWithTarget:(id)target_ action:(SEL)action_
{
	if (self = [super init]) {
		target = target_; // weak-ref
		action = action_;
	}
	return self;
}

@end


@implementation PLTargetActionMessageHandler

+ (PLTargetActionMessageHandler *)handlerWithTarget:(id)target action:(SEL)action
{
	return [[[PLTargetActionMessageHandler alloc] initWithTarget:target action:action] autorelease];
}

- (void)handleMessage:(id)message forPattern:(PLPattern *)pattern
{
	[target performSelector:action withObject:message];
}

@end


@implementation PLTargetActionTimeoutHandler

+ (PLTargetActionTimeoutHandler *)handlerWithTarget:(id)target action:(SEL)action
{
	return [[[PLTargetActionTimeoutHandler alloc] initWithTarget:target action:action] autorelease];
}

- (void)handleTimeoutForPattern:(PLPattern *)pattern;
{
	[target performSelector:action];
}

@end


#pragma mark patterns

@interface PLPredicatePattern : PLPattern
{
	NSPredicate *predicate;
}

- (PLPredicatePattern *)initWithPredicate:(NSPredicate *)pred;

@end


@interface PLOneOfPattern : PLPattern
{
	NSArray *patterns;
}

- (id)initWithPatterns:(NSArray *)patterns;

@end


@implementation PLPattern

+ (PLPattern *)patternAny
{
	return [[[PLPattern alloc] init] autorelease];
}

+ (PLPattern *)patternWithPredicate:(NSPredicate *)predicate
{
	return [[[PLPredicatePattern alloc] initWithPredicate:predicate] autorelease];
}

+ (PLPattern *)patternWithPredicateFormat:(NSString *)predicateFormat, ...
{
	va_list ap;
	va_start(ap, predicateFormat);
	PLPattern *res = [self patternWithPredicate:[NSPredicate predicateWithFormat:predicateFormat arguments:ap]];
	va_end(ap);
	return res;
}

- (void)dealloc
{
	[timeoutTimer release];
	[timeoutHandler release];
	[messageHandler release];
	[super dealloc];
}

- (PLPattern *)setMessageHandler:(id<PLMessageHandler>)handler
{
	[messageHandler autorelease];
	messageHandler = [handler retain];
	return self;
}

- (id<PLMessageHandler>)messageHandler
{
	return messageHandler;
}

- (PLPattern *)setMessageTarget:(id)target action:(SEL)action
{
	return [self setMessageHandler:[PLTargetActionMessageHandler handlerWithTarget:target action:action]];
}

- (PLPattern *)setTimeoutHandler:(id<PLTimeoutHandler>)timeout after:(NSTimeInterval)interval
{
	[timeoutHandler autorelease];
	timeoutHandler = [timeout retain];
	timeoutInterval = interval;
	return self;
}

- (id<PLTimeoutHandler>)timeoutHandler
{
	return timeoutHandler;
}

- (PLPattern *)setTimeoutTarget:(id)target action:(SEL)action after:(NSTimeInterval)interval
{
	return [self setTimeoutHandler:[PLTargetActionTimeoutHandler handlerWithTarget:target action:action] after:interval];
}

- (NSTimeInterval)timeoutInterval
{
	return timeoutInterval;
}

- (void)setTimeoutTimer:(NSTimer *)timer
{
	[timeoutTimer autorelease];
	timeoutTimer = [timer retain];
}

- (void)invalidate
{
	[timeoutTimer invalidate];
}

- (PLPattern *)matches:(id)message
{
	return self; // this is for the any case, overwrite in subclasses!
}

@end


@implementation PLPredicatePattern

- (PLPredicatePattern *)initWithPredicate:(NSPredicate *)pred
{
	if (self = [super init]) {
		predicate = [pred copy];
	}
	return self;
}

- (void)dealloc
{
	[predicate release];
	[super dealloc];
}

- (PLPattern *)matches:(id)message
{
	return [predicate evaluateWithObject:message] ? self : nil;
}

@end


@implementation PLOneOfPattern

- (id)initWithPatterns:(NSArray *)patterns_
{
	if (self = [super init]) {
		patterns = [patterns_ copy];
	}
	return self;
}

- (void)dealloc
{
	[patterns release];
	[super dealloc];
}

- (PLPattern *)matches:(id)message
{
	for (PLPattern *pattern in patterns) {
		if ([pattern matches:message]) {
			return pattern;
		}
	}
	return nil;
}

@end


#pragma mark platform

@interface PLPlatform (InternalMethods)

- (void)postData:(NSData *)data;

- (BOOL)deliverAnyMessageForPattern:(PLPattern *)pattern;
- (BOOL)deliverMessage:(id)message forPattern:(PLPattern *)pattern;

- (id)internalizeFromData:(NSData *)data;
- (NSData *)externalizeToData:(id)message;

- (NSString *)aidForHost:(NSString *)host port:(unsigned short)port number:(NSUInteger)number;

- (void)noteProblem:(NSString *)problem inSubsystem:(NSString *)subsystem;

@end


@implementation PLPlatform

- (id)init
{
	if (self = [super init]) {
		patterns = [[NSMutableArray alloc] init];
		messages = [[NSMutableArray alloc] init];
		endpoint = [[PLEndpoint alloc] init];
		endpoint.delegate = self;
	}
	return self;
}

- (void)dealloc
{
	[endpoint release];
	[patterns release];
	[messages release];
	[super dealloc];
}

- (NSString *)createAid
{
	return [self aidForHost:[endpoint hostname] port:[endpoint port] number:nextLocalAid++];
}

- (void)send:(id)message to:(NSString *)aid
{	
	// convert message to data object
	NSData *data = [self externalizeToData:message];
	if (!data) {
		[NSException raise:NSInvalidArgumentException format:@"message to send is nil or is not in plist format"];
	}
	
	// convert aid to host and port
	NSURL *url = [NSURL URLWithString:aid];
	NSString *host = [url host];
	unsigned short port = [[url port] unsignedShortValue];
	if (!host || !port) {
		[NSException raise:NSInvalidArgumentException format:@"invalid host or port of target aid:%@", aid];
	}
	
	// if we send to same platform, post it directly, otherwise send through endpoint 
	if (port == endpoint.port && [host isEqual:endpoint.hostname]) {
		[self postData:data];
	}
	else {
		[endpoint sendData:data toHost:host port:port];
	}
}

- (PLPattern *)receive:(PLPattern *)pattern
{
	// store away the new pattern
	[patterns addObject:pattern];
	
	// check if this new pattern matches any of the pending messages
	BOOL deliveredAny = [self deliverAnyMessageForPattern:pattern];

	// start timout timers if indicated
	if (!deliveredAny && [pattern timeoutHandler]) {
		NSTimeInterval interval = [pattern timeoutInterval];
		NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval
														  target:self 
														selector:@selector(onTimer:) 
														userInfo:pattern 
														repeats:NO];
		[pattern setTimeoutTimer:timer];
		
		// fire timer immediatly if interval is zero
		if (interval == 0.0) {
			[timer fire];
		}
	}
	
	// return pattern for call chain convenience
	return pattern;
}

- (void)onTimer:(NSTimer *)timer
{
	PLPattern *pattern = [timer userInfo];
	
	// remove pattern from list of patterns
	[pattern retain];
	[patterns removeObject:pattern];
	
	// call the timeout
	[[pattern timeoutHandler] handleTimeoutForPattern:pattern];
	
	// give up upon pattern
	[pattern release];
}

- (PLPattern *)receiveOneOf:(NSArray *)patterns_
{
	return [self receive:[[[PLOneOfPattern alloc] initWithPatterns:patterns_] autorelease]];
}

- (void)endpoint:(PLEndpoint *)endpoint didReceiveData:(NSData *)data from:(NSData *)addr
{
	[self postData:data];
}

- (void)endpoint:(PLEndpoint *)endpoint encounteredProblem:(NSString *)problem
{
	[self noteProblem:problem inSubsystem:@"endpoint"];
}

- (void)postData:(NSData *)data
{
	id message = [self internalizeFromData:data];
	
	for (PLPattern *pattern in [patterns copy]) {
		if ([self deliverMessage:message forPattern:pattern]) {
			return;
		}
	}		
	
	// could not immediatly deliver message, so add to message queue
	[messages addObject:message];
}

- (BOOL)deliverAnyMessageForPattern:(PLPattern *)pattern
{
	for (id message in [messages copy]) {
		if ([self deliverMessage:message forPattern:pattern]) {
			[messages removeObject:message];
			return YES;
		}
	}
	return NO;
}

- (BOOL)deliverMessage:(id)message forPattern:(PLPattern *)pattern
{
	PLPattern *matchedPattern = [pattern matches:message];
	if (matchedPattern) {
		
		// remove pattern from list of patterns but don't free it yet.
		[pattern retain];
		[patterns removeObject:pattern];
		
		// stop any timers
		[pattern invalidate];
		
		// let handler of matched-pattern handle the message
		id<PLMessageHandler> handler = [matchedPattern messageHandler];
		if (handler) {
			[handler handleMessage:message forPattern:matchedPattern];
		}
		
		// now free the pattern
		[pattern release];
		
		return YES;
	}
	else {
		return NO;
	}
}

- (id)internalizeFromData:(NSData *)data
{
	NSString *error;
	id res = [NSPropertyListSerialization propertyListFromData:data 
											  mutabilityOption:NSPropertyListImmutable 
														format:NULL 
											  errorDescription:&error];
	if (error) {
		[self noteProblem:error inSubsystem:@"internalizeData"];
		[error release];
	}
	return res;
}

- (NSData *)externalizeToData:(id)msg
{
	NSString *error;
	NSData *res = [NSPropertyListSerialization dataFromPropertyList:msg 
															 format:NSPropertyListBinaryFormat_v1_0
												   errorDescription:&error];
	if (error) {
		[self noteProblem:error inSubsystem:@"externalizeData"];
		[error release];
	}
	return res;
}

- (NSString *)aidForHost:(NSString *)host port:(unsigned short)port number:(NSUInteger)number
{
	// actor-ids (aids) will be of form: plap://machine:port/number
	return [NSString stringWithFormat:@"plap://%@:%d/%d", host, port, number];
}

- (void)noteProblem:(NSString *)problem inSubsystem:(NSString *)subsystem
{
	NSLog(@"problem in '%@': %@", subsystem, problem);
}

@end
